package core;

import io.XMLInteractor;

import java.util.*;

import messages.ClientPingMessage;
import messages.QuitMessage;
import net.ClientCommunicator;
import user.User;

import commands.Command;
import commands.QuitCommand;

import event.ClientPingEventArgs;
import exceptions.InvalidLoginParametersException;
import exceptions.UserAlreadyExistsException;
import game.Player;


/**
 * UserManager
 * 
 * The user-manager is responsible for managing the list of known players and
 * online clients. 
 */
public class UserManager {
	private static final long serialVersionUID = 1L;
	public UserManager() {
		loadKnownUsers();
		this.loggedUsers = new HashMap<String, User>();

		Timer timer = new Timer();
		timer.schedule(new Pinger(), 20 * 1000, 20 * 1000);
	}


	/**
	 * Adds an user to the list of logged in clients.
	 * 
	 * @param login The login of the user.
	 * @param client The Client instance to allow communication to the client application.
	 */
	public synchronized void setOnline(String login, User client) {
		if (!isLoggedIn(login)) {
			this.loggedUsers.put(login, client);
			this.getPlayer(login).setOnline(true);
		}
	}


	/**
	 * Removes an user from the list of logged in clients.
	 * 
	 * @param login The login of the user.
	 */
	public synchronized void setOffline(String login) {
		if (isLoggedIn(login)) {
			this.loggedUsers.remove(login);
			this.lastSentPing.remove(login);
			this.getPlayer(login).setOnline(false);
		}
	}


	/**
	 * Authenticates and accepts a new user.
	 * Returns an instance of User representing the client.
	 * 
	 * @param message The message containing login information provided by the client.
	 * @param communicator The communicator used to communicate with the client.
	 * @return An User representing the client.
	 */
	public synchronized User authenticate(String login, String password, ClientCommunicator communicator) 
	throws InvalidLoginParametersException {

		User user = null;

		if (isValid(login, password)) {
			if (!isLoggedIn(login)) {

				Player player = getPlayer(login);
				try {
					user = (User) User.getRoleClass(player.getRole()).getConstructors()[0].newInstance(communicator, player);
				} catch (Exception e) {
					// Murphy says this will happen
					e.printStackTrace();
				}

				// Adds the user to the set of online users
				setOnline(login, user);
			} else {
				user = loggedUsers.get(login);
				user.setCommunicator(communicator);
			}
		} else {
			throw new InvalidLoginParametersException(login);
		}

		return user;
	}


	/**
	 * Registers a new player. If the registration is successful, the player is
	 * set online.
	 * 
	 * @param login The username of the player 
	 * @param password The player's password.
	 * 
	 * @return The User representing this player.
	 */
	public synchronized User register(String login, String password, ClientCommunicator communicator)
	throws UserAlreadyExistsException {

		addUser(login, password, "User");

		User user = null;

		try {
			user = authenticate(login, password, communicator);
		} catch (Exception e) {
			// Safely ignore. Neither exception can ocurr :)
		}

		return user;
	}
	
	
	/**
	 * Adds a new player to the list of known users.
	 * 
	 * @param login The username of the new player.
	 * @param password The password of the new player.
	 * @throws UserAlreadyExistsException when the user already exists.
	 */
	public void addUser(String login, String password, String role) throws UserAlreadyExistsException {
		if (!isLoginAvailable(login))
			throw new UserAlreadyExistsException(login);
		
		this.knownUsers.put(login, new Player(login, password, role));
		saveKnownUsers();
	}
	
	
	/**
	 * Removes a player from the list of known users
	 * Doesn't throw any exception when the user doesn't exist
	 * 
	 * @param login The login of the player to remove.
	 */
	public void removeUser(String login) {
		this.knownUsers.remove(login);
		saveKnownUsers();
	}


	/**
	 * Called when the user replied the Ping Message.
	 * 
	 * @param login The login of the user who replied.
	 */
	public synchronized void ackPong(String login) {
		lastSentPing.remove(login);
	}


	/**
	 * Gets a specific player from the list of known players.
	 * 
	 * @param login The login of the player to retrieve.
	 * @return The Player instance of the wanted player.
	 */
	public synchronized Player getPlayer(String login) {
		return this.knownUsers.get(login);
	}


	/**
	 * Returns an user from the list of online users.
	 * 
	 * @param login The login that identifies the user to obtain.
	 * @return The wanted User instance.
	 */
	public synchronized User getUser(String login) {
		return this.loggedUsers.get(login);
	}


	/**
	 * Returns the list of all players.
	 * 
	 * @return The list of all players.
	 */
	public synchronized List<Player> getPlayers() {
		return new ArrayList<Player>(knownUsers.values());
	}
	
	
	/**
	 * Returns the list of online players.
	 * 
	 * @return The list of online players.
	 */
	public synchronized List<Player> getOnlinePlayers() {
		List<Player> list = new ArrayList<Player>();
		for (User user : loggedUsers.values()) {
			list.add(user.getPlayer());
		}

		return list;
	}


	/**
	 * Checks if the password for a given user is correct.
	 * 
	 * @param login The login of the user to check.
	 * @param password The corresponding password.
	 * @return True if the password if correct. False otherwise or if the login is unknown.
	 */
	public synchronized boolean isValid(String login, String password) {
		if (isLoginAvailable(login))
			return false;

		Player player = this.knownUsers.get(login);

		return player.getPassword().equals(password);
	}


	/**
	 * Is a client with a given login online?
	 * 
	 * @param login The login to check
	 * @return True if the client is logged in, false otherwise 
	 */
	public synchronized boolean isLoggedIn(String login) {
		return this.loggedUsers.containsKey(login);
	}


	/**
	 * Is a login available for registration?
	 * 
	 * @param login The login to check
	 * @return True if the login is available, false otherwise 
	 */
	public synchronized boolean isLoginAvailable(String login) {
		return !this.knownUsers.containsKey(login);
	}


	/**
	 * Initializes the list of known users
	 */
	@SuppressWarnings("unchecked")
	private synchronized void loadKnownUsers() {
		this.knownUsers = XMLInteractor.loadPlayers();

	}


	/**
	 * Saves the list of known users
	 */
	public synchronized void saveKnownUsers() {
		XMLInteractor.savePlayers(knownUsers.values());
	}
	

	private Map<String, Player> knownUsers;
	private Map<String, User> loggedUsers;
	private Map<String, Date> lastSentPing = new HashMap<String, Date>();


	private class Pinger extends TimerTask {

		@Override
		public void run() {

			synchronized (UserManager.this) {

				if (loggedUsers.isEmpty())
					return;

				// First, let's see if any user timed out...
				Calendar now = new GregorianCalendar();
				Calendar then = new GregorianCalendar();
				List<String> toRemove = new ArrayList<String>();

				for (String userLogin : lastSentPing.keySet()) {
					then.setTime(lastSentPing.get(userLogin));
					then.add(Calendar.MINUTE, 1);

					if (now.after(then)) {
						toRemove.add(userLogin);
					}	
				}

				// Now let's log them out...
				for (String userLogin : toRemove) {
					// ... unless they already logged out.
					if (loggedUsers.containsKey(userLogin)) {	
						System.err.println(userLogin + " timedout");
						Command command = new QuitCommand(new QuitMessage(), loggedUsers.get(userLogin));
						command.execute();
					}

					lastSentPing.remove(userLogin);
				}	

				// Finally: let's ping remaining users!
				for (User user : loggedUsers.values()) {
					if (!lastSentPing.containsKey(user.getLogin()))
						lastSentPing.put(user.getLogin(), new Date());
				}

				Controller.getInstance().fireClientPingEvent(
						new ClientPingEventArgs(new ClientPingMessage()));
			}
		}
	}


}
